﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using LitJson;

public class PUEXUtils {

	public static void vec3MakeFloor (ref Vector3 v, Vector3 cmp)
	{
		if( cmp.x < v.x ) 
			v.x = cmp.x;
		if( cmp.y < v.y ) 
			v.y = cmp.y;
		if( cmp.z < v.z ) 
			v.z = cmp.z;
	}

	public static void vec3MakeCeil (ref Vector3 v, Vector3 cmp)
	{
		if( cmp.x > v.x ) 
			v.x = cmp.x;
		if( cmp.y > v.y ) 
			v.y = cmp.y;
		if( cmp.z > v.z ) 
			v.z = cmp.z;
	}
	
	public static float vec3AbsDotProduct(Vector3 src, Vector3 vec)
	{
		return Mathf.Abs(src.x * vec.x) + Mathf.Abs(src.y * vec.y) + Mathf.Abs(src.z * vec.z);
	}

	public static Vector3 perp(Vector3 v) 
	{
		float min = Mathf.Abs(v.x);
		Vector3 cardinalAxis = new Vector3(1, 0, 0);
		
		if (Mathf.Abs(v.y) < min) {
			min = Mathf.Abs(v.y);
			cardinalAxis = new Vector3(0, 1, 0);
		}
		
		if (Mathf.Abs(v.z) < min) {
			cardinalAxis = new Vector3(0, 0, 1);
		}
		
		Vector3 ret = Vector3.Cross(v, cardinalAxis);
		return ret;
	}
	
	public static Vector3 randomDeviant (Vector3 src, float angle)
	{
		Vector3 newUp = perp(src);

		// Rotate up vector by random amount around this
		Quaternion q = Quaternion.AngleAxis (Random.value * 360f, src);
		newUp = q * newUp;
		
		// Finally rotate this by given angle around randomised up
		q = Quaternion.AngleAxis (Random.value * angle, newUp);
		return q * src;
	}

	public static float gaussianDistribution(float x, float offset, float scale)
	{
		float nom = Mathf.Exp(-Mathf.Sqrt(x - offset) / (2f * Mathf.Sqrt(scale)));
		float denom = scale * Mathf.Sqrt(2f * Mathf.PI);
		
		return nom / denom;
	}

	public static float boundingRadiusFromAABB (Bounds aabb)
	{
		Vector3 max = aabb.max;
		Vector3 min = aabb.min;
		
		Vector3 mag = max;
		vec3MakeCeil(ref mag, -max);
		vec3MakeCeil(ref mag, min);
		vec3MakeCeil(ref mag, -min);
		
		return mag.magnitude;
	}

	public static bool intersects (Sphere sphere, Bounds box)
	{
		// Use splitting planes
		Vector3 center = sphere.center;
		float radius = sphere.radius;
		Vector3 min = box.min;
		Vector3 max = box.max;
		
		// Arvo's algorithm
		float s, d = 0;

		if (center.x < min.x)
		{
			s = center.x - min.x;
			d += s * s; 
		}
		else if(center.x > max.x)
		{
			s = center.x - max.x;
			d += s * s; 
		}

		if (center.y < min.y)
		{
			s = center.y - min.y;
			d += s * s; 
		}
		else if(center.y > max.y)
		{
			s = center.y - max.y;
			d += s * s; 
		}

		if (center.z < min.z)
		{
			s = center.z - min.z;
			d += s * s; 
		}
		else if(center.z > max.z)
		{
			s = center.z - max.z;
			d += s * s; 
		}

		return d <= radius * radius;
	}

	// BOTH_SIDE = 0, POSITIVE_SIDE = 1, NEGATIVE_SIDE = 2
	public static int planeGetSideFromAABB (Plane plane, Bounds box)
	{
		Vector3 centre = box.center;
		Vector3 halfSize = box.extents;

		// Calculate the distance between box centre and the plane
		float dist = plane.GetDistanceToPoint (centre);
		
		// Calculate the maximise allows absolute distance for
		// the distance between box centre and plane
		float maxAbsDist = vec3AbsDotProduct (plane.normal, halfSize);
		
		if (dist < -maxAbsDist)
			return 2;
		
		if (dist > +maxAbsDist)
			return 1;
		
		return 0;
	}

	public static bool intersects (Plane plane, Bounds box)
	{
		return planeGetSideFromAABB (plane, box) == 0;
	}

	public static bool intersects (Sphere sphere, Plane plane)
	{
		return plane.GetDistanceToPoint (sphere.center) <= sphere.radius;
	}

	public static Vector3 planeProjectVector (Plane plane, Vector3 vec)
	{
		Matrix4x4 xform = Matrix4x4.identity;
		xform.SetRow (0, new Vector4 (1.0f - plane.normal.x * plane.normal.x, -plane.normal.x * plane.normal.y, -plane.normal.x * plane.normal.z, 0f));
		xform.SetRow (1, new Vector4 (-plane.normal.y * plane.normal.x, 1.0f - plane.normal.y * plane.normal.y, -plane.normal.y * plane.normal.z, 0f));
		xform.SetRow (2, new Vector4 (-plane.normal.z * plane.normal.x, -plane.normal.z * plane.normal.y, 1.0f - plane.normal.z * plane.normal.z, 0f));
		return xform * vec;
	}

	public static float matrixDeterminant (Matrix4x4 mat)
	{
		float a0 = mat[0] * mat[5] - mat[1] * mat[4];
		float a1 = mat[0] * mat[6] - mat[2] * mat[4];
		float a2 = mat[0] * mat[7] - mat[3] * mat[4];
		float a3 = mat[1] * mat[6] - mat[2] * mat[5];
		float a4 = mat[1] * mat[7] - mat[3] * mat[5];
		float a5 = mat[2] * mat[7] - mat[3] * mat[6];
		float b0 = mat[8] * mat[13] - mat[9] * mat[12];
		float b1 = mat[8] * mat[14] - mat[10] * mat[12];
		float b2 = mat[8] * mat[15] - mat[11] * mat[12];
		float b3 = mat[9] * mat[14] - mat[10] * mat[13];
		float b4 = mat[9] * mat[15] - mat[11] * mat[13];
		float b5 = mat[10] * mat[15] - mat[11] * mat[14];
		
		// Calculate the determinant.
		return (a0 * b5 - a1 * b4 + a2 * b3 + a3 * b2 - a4 * b1 + a5 * b0);
	}
	
	public static bool matrixDecompose (Matrix4x4 mat, ref Vector3 scale, ref Quaternion rotation, ref Vector3 translation)
	{
//		if (translation != null)
//		{
			// Extract the translation.
			translation.x = mat[12];
			translation.y = mat[13];
			translation.z = mat[14];
//		}
		
//		// Nothing left to do.
//		if (scale == null && rotation == null)
//			return true;
		
		// Extract the scale.
		// This is simply the length of each axis (row/column) in the matrix.
		Vector3 xaxis = new Vector3(mat[0], mat[1], mat[2]);
		float scaleX = xaxis.magnitude;
		
		Vector3 yaxis = new Vector3(mat[4], mat[5], mat[6]);
		float scaleY = yaxis.magnitude;
		
		Vector3 zaxis = new Vector3(mat[8], mat[9], mat[10]);
		float scaleZ = zaxis.magnitude;
		
		// Determine if we have a negative scale (true if determinant is less than zero).
		// In this case, we simply negate a single axis of the scale.
		float det = matrixDeterminant(mat);
		if (det < 0)
			scaleZ = -scaleZ;
		
//		if (scale != null)
//		{
			scale.x = scaleX;
			scale.y = scaleY;
			scale.z = scaleZ;
//		}
		
//		// Nothing left to do.
//		if (rotation == null)
//			return true;
		
		// Scale too close to zero, can't decompose rotation.
		if (scaleX < Mathf.Epsilon || scaleY < Mathf.Epsilon || Mathf.Abs(scaleZ) < Mathf.Epsilon)
			return false;
		
		float rn;
		
		// Factor the scale out of the matrix axes.
		rn = 1.0f / scaleX;
		xaxis.x *= rn;
		xaxis.y *= rn;
		xaxis.z *= rn;
		
		rn = 1.0f / scaleY;
		yaxis.x *= rn;
		yaxis.y *= rn;
		yaxis.z *= rn;
		
		rn = 1.0f / scaleZ;
		zaxis.x *= rn;
		zaxis.y *= rn;
		zaxis.z *= rn;
		
		// Now calculate the rotation from the resulting matrix (axes).
		float trace = xaxis.x + yaxis.y + zaxis.z + 1.0f;
		
		if (trace > Mathf.Epsilon)
		{
			float s = 0.5f / Mathf.Sqrt(trace);
			rotation.w = 0.25f / s;
			rotation.x = (yaxis.z - zaxis.y) * s;
			rotation.y = (zaxis.x - xaxis.z) * s;
			rotation.z = (xaxis.y - yaxis.x) * s;
		}
		else
		{
			// Note: since xaxis, yaxis, and zaxis are normalized, 
			// we will never divide by zero in the code below.
			if (xaxis.x > yaxis.y && xaxis.x > zaxis.z)
			{
				float s = 0.5f / Mathf.Sqrt(1.0f + xaxis.x - yaxis.y - zaxis.z);
				rotation.w = (yaxis.z - zaxis.y) * s;
				rotation.x = 0.25f / s;
				rotation.y = (yaxis.x + xaxis.y) * s;
				rotation.z = (zaxis.x + xaxis.z) * s;
			}
			else if (yaxis.y > zaxis.z)
			{
				float s = 0.5f / Mathf.Sqrt(1.0f + yaxis.y - xaxis.x - zaxis.z);
				rotation.w = (zaxis.x - xaxis.z) * s;
				rotation.x = (yaxis.x + xaxis.y) * s;
				rotation.y = 0.25f / s;
				rotation.z = (zaxis.y + yaxis.z) * s;
			}
			else
			{
				float s = 0.5f / Mathf.Sqrt(1.0f + zaxis.z - xaxis.x - yaxis.y );
				rotation.w = (xaxis.y - yaxis.x ) * s;
				rotation.x = (zaxis.x + xaxis.z ) * s;
				rotation.y = (zaxis.y + yaxis.z ) * s;
				rotation.z = 0.25f / s;
			}
		}
		
		return true;
	}

	public static Vector3 closestPointOnLine (Vector3 vA, Vector3 vB, Vector3 vP)
	{
		Vector3 v1 = vP - vA;
		Vector3 v2 = (vB - vA).normalized;

		float d = Vector3.Distance (vA, vB);
		float t = Vector3.Dot (v2, v1);

		if (t <= 0)
			return vA;

		if (t >= d)
			return vB;

		Vector3 v3 = v2 * t;

		return vA + v3;
	}

	public static Vector3 closestPointOnVector (Vector3 vA, Vector3 vB, Vector3 vP)
	{
		Vector3 v1 = vP - vA;
		Vector3 v2 = (vB - vA).normalized;

		float t = Vector3.Dot (v2, v1);
		
		Vector3 v3 = v2 * t;
		
		return vA + v3;
	}
	
	#region - Editor only
	public static void drawCone(Vector3 d, Vector3 a, float h, float rd, int n, Color color)
	{
		Vector3 c = a + (-d * h);
		Vector3 e0 = perp(d);
		Vector3 e1 = Vector3.Cross(e0, d);
		float angInc = 360.0f / n * (Mathf.PI / 180.0f);
		
		// calculate points around directrix
		List<Vector3> pts = new List<Vector3>();
		for (int i = 0; i < n; ++i) {
			float rad = angInc * i;
			Vector3 p = c + (((e0 * Mathf.Cos(rad)) + (e1 * Mathf.Sin(rad))) * rd);
			pts.Add(p);
		}
		
		Gizmos.color = color;
		
		// draw top
		for (int i = 0; i < n; ++i) {
			Gizmos.DrawLine(a, pts[i]);
		}
		
		// draw bottom
		if (n > 0)
		{
			for (int i = n-1; i >= 1; --i) {
				Gizmos.DrawLine(pts[i], pts[i-1]);
			}
			Gizmos.DrawLine(pts[0], pts[n-1]);
		}
	}

	public static JsonData serializeVector2 (Vector2 value)
	{
		JsonData ret = new JsonData ();
		ret ["x"] = value.x;
		ret ["y"] = value.y;
		return ret;
	}

	public static JsonData serializeVector3 (Vector3 value)
	{
		JsonData ret = new JsonData ();
		ret ["x"] = value.x;
		ret ["y"] = value.y;
		ret ["z"] = value.z;
		return ret;
	}

	public static JsonData serializeQuaternion (Quaternion value)
	{
		JsonData ret = new JsonData ();
		ret ["x"] = value.x;
		ret ["y"] = value.y;
		ret ["z"] = value.z;
		ret ["w"] = value.w;
		return ret;
	}

	public static JsonData serializeColor (Color value)
	{
		JsonData ret = new JsonData ();
		ret ["r"] = value.r;
		ret ["g"] = value.g;
		ret ["b"] = value.b;
		ret ["a"] = value.a;
		return ret;
	}

	public static JsonData serializeDynamicAttribute (PUEXDynamicAttribute value)
	{
		JsonData ret = new JsonData ();
		switch (value.type) {
		case PUEXDynamicAttribute.DynamicAttributeType.DAT_FIXED: {
			ret ["type"] = "fixed";
			ret ["param_name"] = value.paramName;
			ret ["value"] = value.value;

			break;
		}
		case PUEXDynamicAttribute.DynamicAttributeType.DAT_RANDOM: {
			ret ["type"] = "random";
			ret ["param_name"] = value.paramName;
			ret ["param2_name"] = value.param2Name;
			ret ["value"] = value.value;
			ret ["value2"] = value.value2;

			break;
		}
		case PUEXDynamicAttribute.DynamicAttributeType.DAT_CURVED: {
			ret ["type"] = "curved";
			ret ["param_name"] = value.paramName;
			ret ["param2_name"] = value.param2Name;
			ret ["value"] = value.value;
			ret ["value2"] = value.value2;
			ret ["curve"] = serializeAnimationCurve(value.curve);

			break;
		}
		}
		return ret;
	}

	public static JsonData serializeDynamicTrigger (PUEXDynamicTrigger value)
	{
		JsonData ret = new JsonData ();

		ret ["base_param_name"] = value.triggerBaseValueParamName;
		ret ["scale_param_name"] = value.triggerScaleValueParamName;
		ret ["base"] = value.baseValue;
		ret ["scale"] = value.scaleValue;
		ret ["curve"] = serializeAnimationCurve(value.curve);

		return ret;
	}

	public static JsonData serializeDynamicColor (PUEXDynamicColor value)
	{
		JsonData ret = new JsonData ();
		switch (value.type) {
		case PUEXDynamicColor.DynamicColorType.DCT_FIXED: {
			ret ["type"] = "fixed";
			ret ["param_name"] = value.paramName;
			ret ["value"] = serializeColor(value.value);

			break;
		}
		case PUEXDynamicColor.DynamicColorType.DCT_RANDOM: {
			ret ["type"] = "random";
			ret ["param_name"] = value.paramName;
			ret ["param2_name"] = value.param2Name;
			ret ["value"] = serializeColor(value.value);
			ret ["value2"] = serializeColor(value.value2);

			break;
		}
		case PUEXDynamicColor.DynamicColorType.DCT_GRADIENT: {
			ret ["type"] = "gradient";
			ret ["gradient"] = serializeGradient(value.gradient);

			break;
		}
		}
		return ret;
	}

	public static JsonData serializeAnimationCurve (AnimationCurve value)
	{
		JsonData ret = new JsonData ();

		if (value.preWrapMode == WrapMode.Loop)
			ret ["pre_wrap_mode"] = "loop";
		else if (value.preWrapMode == WrapMode.PingPong)
			ret ["pre_wrap_mode"] = "pingpong";
//		else if (value.preWrapMode == WrapMode.Clamp)
//			ret ["pre_wrap_mode"] = "clamp";
//		else if (value.preWrapMode == WrapMode.ClampForever)
//			ret ["pre_wrap_mode"] = "clamp_forever";
//		else if (value.preWrapMode == WrapMode.Default)
//			ret ["pre_wrap_mode"] = "default";
//		else if (value.preWrapMode == WrapMode.Once)
//			ret ["pre_wrap_mode"] = "once";
		else if (value.preWrapMode == WrapMode.ClampForever)
			ret ["pre_wrap_mode"] = "clamp";

		if (value.postWrapMode == WrapMode.Loop)
			ret ["post_wrap_mode"] = "loop";
		else if (value.postWrapMode == WrapMode.PingPong)
			ret ["post_wrap_mode"] = "pingpong";
//		else if (value.postWrapMode == WrapMode.Clamp)
//			ret ["post_wrap_mode"] = "clamp";
//		else if (value.postWrapMode == WrapMode.ClampForever)
//			ret ["post_wrap_mode"] = "clamp_forever";
//		else if (value.postWrapMode == WrapMode.Default)
//			ret ["post_wrap_mode"] = "default";
//		else if (value.postWrapMode == WrapMode.Once)
//			ret ["post_wrap_mode"] = "once";
		else if (value.postWrapMode == WrapMode.ClampForever)
			ret ["post_wrap_mode"] = "clamp";

		JsonData keysArray = new JsonData ();
		for (int i = 0; i < value.length; i++) {
			JsonData key = new JsonData ();

			Keyframe kf = value[i];
			if (float.IsInfinity(kf.inTangent))
				key["i"] = "inf";
			else
				key["i"] = kf.inTangent;
			if (float.IsInfinity(kf.outTangent))
				key["o"] = "inf";
			else
				key["o"] = kf.outTangent;
			key["t"] = kf.time;
			key["v"] = kf.value;

			keysArray.Add (key);
		}
		ret ["keys"] = keysArray;

		return ret;
	}

	public static JsonData serializeGradient (Gradient value)
	{
		JsonData ret = new JsonData ();

		JsonData alphaArray = new JsonData ();
		for (int i = 0; i < value.alphaKeys.Length; i++) {
			JsonData alpha = new JsonData ();

			GradientAlphaKey key = value.alphaKeys[i];
			alpha["a"] = key.alpha;
			alpha["t"] = key.time;

			alphaArray.Add(alpha);
		}
		ret ["alpha"] = alphaArray;

		JsonData colorArray = new JsonData ();
		for (int i = 0; i < value.colorKeys.Length; i++) {
			JsonData color = new JsonData ();

			GradientColorKey key = value.colorKeys[i];
			color["r"] = key.color.r;
			color["g"] = key.color.g;
			color["b"] = key.color.b;
			color["t"] = key.time;

			colorArray.Add (color);
		}
		ret ["color"] = colorArray;

		return ret;
	}
	#endregion

}
